fix(lint): 字段级 *When 的用户根拒绝覆盖 ADR-0068 的全部三种拼写 (#6585) - #6711
Conversation
#6290(PR #6584)给字段级 `visibleWhen` / `readonlyWhen` / `requiredWhen` 加了一条按面的用户根拒绝,处方也指向真实存在的面。但它只匹配 `current_user` 一个拼写,而 ADR-0068 D1 把 `user` / `ctx.user` 定为**同一个对象的别名** (`buildScope` 把同一个 `EvalUser` 引用挂在四个名字下)。于是同一个语义错误 在一种拼写下报错、在另外两种下**完全静默** —— 两者一直在 `SCOPE_ROOTS` 里, 裸引用检查也从不报它们。作者随手挑了三个 ADR 认定等价的拼写里的哪一个, 决定了他拿不拿得到构建期诊断。 失败方向就是 #6146 那个:未绑定根 ⇒ fault ⇒ 可见性 fallback 为 `true`, 本想按角色藏起来的字段对所有人恒可见,且无任何信号。 三个根现在共用一条判定、一条处方、一条文案,只有文案里点名的那个根不同。 选项级不受影响:per-option `visibleWhen` 走宿主谓词作用域,那里每种拼写都 绑定用户,所以 showcase 的角色门控选项在别名下同样合法(已钉)。 **`ctx` 按整根判,而不是只判 `ctx.user` 形态。** 在这个面上这就是事实: `buildScope` 只在求值携带用户时才创建 `ctx` 根,而字段级没有任何调用点传 用户 —— 服务端绑 `record` + `previous`(+ `parent`),客户端 `evalFieldPredicate` 绑 `record` + `previous` + 调用方 scope,而后者实测 只可能是 `{ parent }`。所以 `ctx.locale` 与 `ctx.user.id` 在这里同样 fault。 否掉窄读法的理由:它需要源码层面的拼写匹配,而拼写匹配正是本条规则要消灭 的东西 —— 那会把同一个分岔下移一层(`ctx["user"].id` 静默、`ctx.user.id` 报错),同时为了一个更窄的规则**名字**放掉一个真实的 fail-open fault。 `ctx` 在别处仍是 ActionEngine 的谓词根,不受影响:平台自己的 `ctx.user` 谓词全在 action `visible` 上(`sys-user.object.ts`、`sys-invitation.object.ts`), 本规则从不读那个面,该放行已钉为测试。 实测扫描:字段级 `*When` 读取任一用户根的用例,在 `examples/`、`packages/` 与下游 `objectui` 仓中均为**零** —— 按槽位名扫一遍、再按别名拼写本身扫 一遍(防止带逗号的谓词藏起来),两遍都是零。三个示例 app 的 `objectstack validate` 全绿,无新增发现。 反向验证(方向在运行前已预判):把根集合改回 `['current_user']`,新增用例 中恰好 9 条转红,失败形态是 `expected [] to have a length of 1 but got +0` —— 即完全静默,正是本单描述的缺陷;三条接受类钉子(选项级别名、action `ctx.user`、`record` 成员同名)在两个版本下都绿,因为它们断言的是零发现。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
座位验收(
所以「整根」不是口味,是这个面上的事实: 反向验证的形态尤其对:9 条转红,失败是 继承处置合规:读了前一版 diff、逐条独立复核后才建,并交代了五处修正。其中第 5 条(root-vs-MEMBER 歧视钉, changeset 判断也对:已发布包的对外行为变更(此前静默通过的元数据现在报错),与同文件同规则的直接先例 #6584 一致, #6713 已收到并复核,是一条有分量的发现 —— 尤其
工作树 CI 收敛后由我摘草稿并入队。 Generated by Claude Code |
Fixes #6585
缺陷
PR #6584 给字段级
visibleWhen/readonlyWhen/requiredWhen加了checkFieldRuleUserRoot—— 一条按面给出的拒绝,处方也指向真实存在的面。但它只匹配
current_user一个拼写:而 ADR-0068 D1 把
user/ctx.user定为同一个EvalUser对象的别名 ——formula/stdlib.ts的buildScope把同一个引用同时挂在current_user/user/ctx.user/os.user下。于是同一个语义错误,写current_user报错,写
user或ctx.user完全静默(两者一直在SCOPE_ROOTS里,裸引用检查也从不报它们)。作者随手挑了三个 ADR 认定等价的拼写里的哪一个,
决定了他拿不拿得到构建期诊断 —— 这正是 AI 作者最难自查的一类分岔。
失败方向就是 #6146 那个:未绑定根 ⇒ fault ⇒ 可见性 fallback 为
true⇒本想按角色藏起来的字段对所有人恒可见,且无任何构建期信号。
改动
走路线 1(路线 2 已按分诊维持否决):根集合从一个拼写扩到三个,共用一条
判定、一条处方、一条文案 —— 只有文案里点名的那个根不同,没有按拼写
分叉。选项级不受影响:per-option
visibleWhen走宿主谓词作用域,那里每种拼写都绑定用户,showcase 的角色门控选项在别名下同样合法(已钉为测试)。
ctx的显式决定:按整根判,不是只判ctx.user形态决定:整根。 依据是实测,不是偏好:
buildScope只在if (ctx.user !== undefined)内部才创建scope.ctx(
stdlib.ts:311-323)。字段级没有任何调用点传用户,所以在这个面上ctx根压根不存在 ——ctx.locale与ctx.user.id同样 fault。readonlyWhen走rule-validator.ts:577(
{ record, previous, extra: { parent } }),requiredWhen走:1403(
{ record, previous, ...parentScope })。都没有 user。对照组:选项级visibleWhen走:1300,签名里有user—— 这就是两个面判定相反的原因。evalFieldPredicate传extra: scope,而 objectui 里全部五个字段级调用点(
form.tsx×3、WizardForm.tsx、GridField.tsx)传的
scope只可能是undefined或{ parent: contextRecord }。FIELD_RULE_ROOTS = ['record','previous','parent'](
ObjectFieldInspector.tsx:127)—— 一个白名单,注释明写 "nocurrent_user"。ctx.user形态)被否:collectCelRootIdentifiers按设计只报ROOT、丢掉成员名,所以窄读法需要源码层面的拼写匹配 —— 而拼写匹配正是
本条规则要消灭的东西。那会把同一个分岔下移一层(
ctx["user"].id静默、ctx.user.id报错),同时为了一个更窄的规则名字放掉一个真实的fail-open fault(
ctx.locale)。ctx在别处仍是 ActionEngine 的谓词根,不受影响:平台自己的ctx.user谓词全在 action
visible上(sys-user.object.ts×8、sys-invitation.object.ts×2、sys-user.page.ts×1),本规则从不读那个面。两个方向都已钉进测试。
实测扫描(派单前置 a)
字段级
*When读取任一用户根的用例:全域为零。两遍独立扫描:examples/*When,含用户根 0ctx.*命中全是 JS hook/script 源码(ctx.api/ctx.ql/ctx.logger),非 CEL;current_user仅 2 处,都在别的面packages/*When(非测试),含用户根 0.describe()文案与json-schema/生成物;真实元数据里的ctx.user全在 actionvisibleobjectui(下游)*When,含用户根 0current_user命中全是选项级visibleWhen;ctx.user命中全是 actionvisible/ 容器 / kanban 卡片谓词仅有的两处合法
current_user用法都在别的面上,与 #6585 正文一致:my-work.page.ts:85(page component)与cascading-select.object.ts:85(option 级)。结论:扩宽不会拒绝任何在别处能工作的东西。
反向验证(方向在运行前已预判)
预判:把
FIELD_UNBOUND_USER_ROOTS改回['current_user'],应有 9 条新增用例转红(3 槽 ×
user、3 槽 ×ctx.user、裸ctx、2 条处方一致性),而3 条接受类钉子保持绿,因为它们断言的是零发现,两个版本下都成立。
实测完全吻合 ——
Tests 9 failed | 161 passed,失败形态是:即完全静默,而不是文案不同 —— 正是本单描述的缺陷形态。恢复后 170/170 全绿。
测试
新增 12 条(
validate-expressions.test.ts),含两条方向相反的钉子:ctx.locale在字段级被拒(整根决定的一面)、ctx.user在 actionvisible上仍被接受(另一面)。另加一条 root-vs-成员歧视钉:
record.user_id/record.ctx_key不得误伤 —— 整根扩宽让这条歧视新变得关键。Changeset
已加(
.changeset/field-when-user-root-aliases.md,@objectstack/lint: patch)。理由:这是已发布包的对外行为变更 —— 此前静默通过的元数据现在会在
objectstack validate上报错,消费者可见。且与直接先例 #6584 一致(同文件、同规则,该 PR 也发了 changeset)。
门禁
pnpm --filter @objectstack/lint test——Test Files 63 passed (63)/Tests 1620 passed | 4 skippedpnpm --filter @objectstack/lint typecheck—— 干净pnpm validate——app-showcase/app-crm/app-todo全部✓ Validation passed,零新增发现node scripts/check-nul-bytes.mjs—— OK(6237 个文件);另对本 PR 三个文件做了超出门禁的控制字符自扫,零命中Refs: #6290、#6584、#6146、ADR-0068 D1、objectui#1582
Generated by Claude Code